package main 

/**
参考文档：
	mysql orm: https://gorm.io/docs/delete.html
		go get -u github.com/jinzhu/gorm
	web应用gin: https://github.com/gin-gonic/gin
		go get -u github.com/gin-gonic/gin

说明：
	1、目的是练习golang基于gin+gorm的web开发涉及到的最基本的增删改查，所以关于数据库连接和对应配置从配置文件的读取没有详细说明
	如果想了解配置文件解析和读取可以参考： https://www.jianshu.com/p/eaed19c3577f
	2、为了方便演示，所有的代码都在同一个文件todo.go中，没有做项目目录的划分，关于项目目录划分后续文章介绍
	3、为了保证http handle 逻辑和 实际model的正删改查不混在一起，所以这里定义了 service和handler
		service 是 model的正删改查逻辑
		handler 是 http 请求结构的逻辑
*/

import (
	"github.com/jinzhu/gorm"
	_ "github.com/jinzhu/gorm/dialects/mysql"

	"github.com/gin-gonic/gin"

	"fmt"
	"log"
	"time"
	"net/http"
	"strconv"
)

// 定义全局变量
var (
	db	*gorm.DB
	err	error
)

// todo model
type Todo struct {
	gorm.Model
	Title	string	`gorm:"type:varchar(200)"`
	Status	uint	`gorm:"type:tinyint"`
}

// 数据库表明自定义，默认为model的复数形式，比如这里默认为 todos
func (Todo) TableName() string {
	return "gocrud_todo"
}


// mysql connection
func ConnectDB() {
	db, err = gorm.Open("mysql", "bamboo:Bamboo@tcp(127.0.0.1:3306)/bamboo?charset=utf8mb4&parseTime=True&loc=Local")
	if err != nil {
		log.Panicln("连接数据库报错" + err.Error())
	}

	// 最大空闲数
	db.DB().SetMaxIdleConns(10)
	// 最大连接数
	db.DB().SetMaxOpenConns(50)
	// 最长连接时间
	db.DB().SetConnMaxLifetime(5 * time.Minute)
	// open log
	db.LogMode(true)
}

func DisconnectDB() {
	if err = db.Close(); err != nil {
		log.Println("数据库关闭失败：" + err.Error())
	}
}
// service
func AddTodoService(todo *Todo) error {
	return db.Create(todo).Error
}

func UpdateTodoService(todo *Todo, id uint) error {
	return db.Debug().Model(todo).Where("id = ?", id).Updates(map[string]interface{}{
		"title": todo.Title,
		"status": todo.Status,
	}).Error
}

func DeleteTodoService(id int) error {
	return db.Delete(&Todo{}, id).Error
}

func GetTodoService(id int) (*Todo, error) {
	var todo Todo
	err = db.Debug().Where("id = ?", id).First(&todo).Error
	if err != nil {
		fmt.Println("获取详细失败", err )
		return nil, err 
	}
	return &todo, nil
}

func ListTodoService()([]*Todo, error) {
	var todos []*Todo

	err = db.Debug().Find(&todos).Error
	if err != nil {
		fmt.Println("获取列表失败", err)
		return nil, err
	} else {
		return todos, nil
	}
}

// handler
func indexHandler(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{
		"msg": "success",
		"code": 200,
		"data": "index",
	})
}

type TodoHttpAddHandler struct {
	Title	string 	`json:"title"`
}
func addTodoHandler(c *gin.Context) {
	var newTodo TodoHttpAddHandler
	if err := c.Bind(&newTodo); err != nil {
		c.JSON(http.StatusOK, gin.H{
			"msg": "bind error",
			"code": 5002,
			"data": "",
		})
	} else {
		todo := &Todo{
			Title: newTodo.Title,
			Status: 0,
		}
		if err := AddTodoService(todo); err != nil {
			c.JSON(http.StatusOK, gin.H{
				"msg": "add item error",
				"code": 5004,
				"data": "",
			})
		} else {
			c.JSON(http.StatusOK, gin.H{
				"msg": "success",
				"code": 200,
				"data": todo,
			})
		}
	}
}
func getTodoHandler(c *gin.Context) {
	var (
		todo *Todo
		err error
	)

	id := c.Param("id")
	idInt, _ := strconv.Atoi(id)
	
	todo, err = GetTodoService(idInt)
	fmt.Println(todo)
	if err != nil {
		fmt.Println("接口获取详细失败 ", err)
		c.JSON(http.StatusOK, gin.H{
			"msg": "获取详情失败",
			"code": 5001,
			"data": "",
		})
	} else {
		c.JSON(http.StatusOK, gin.H{
			"msg": "success",
			"code": 200,
			"data": todo,
		})
	}
}

type TodoHttpUpdateHandler struct {
	Title	string	`json:"title"`
	Status	uint 	`json:"status"`
}
func updateTodoHandler(c *gin.Context) {
	id := c.Param("id")
	idInt, _ := strconv.Atoi(id)
	idUint64, _ := strconv.ParseUint(id, 10, 64)
	idUint := uint(idUint64)
	var updateTodo TodoHttpUpdateHandler
	if err := c.Bind(&updateTodo); err != nil {
		c.JSON(http.StatusOK, gin.H{
			"msg": "bind error",
			"code": 5005,
			"data": "",
		})
	} else {
		_, err = GetTodoService(idInt)
		// 更新的如果不存在
		if err != nil {
			c.JSON(http.StatusOK, gin.H{
				"msg": "待更新记录不存在",
				"code": 5007,
				"data": "",
			})
		} else {
			todo := &Todo{
				Title: updateTodo.Title,
				Status: updateTodo.Status,
			}
			// 如果 todo.ID = idUint 放到这里则SQL为
			// UPDATE `gocrud_todo` SET `status` = 0, `title` = '这是第3个测试的todo item，你搞错啦4', `updated_at` = '2020-12-23 16:15:59'  WHERE `gocrud_todo`.`deleted_at` IS NULL AND `gocrud_todo`.`id` = 3 AND ((id = 3))
			// 多了一个 AND `gocrud_todo`.`id` = 3
			if err := UpdateTodoService(todo, idUint); err != nil {
				c.JSON(http.StatusOK, gin.H{
					"msg": "update item error",
					"code": 5006,
					"data": "",
				})
			} else {
				// 注意：这里todo.ID 位置不同，实际对应的数据库执行的SQL语句不同
				// 这里对应的SQL是
				// UPDATE `gocrud_todo` SET `status` = 1, `title` = '这是第3个测试的todo item，你搞错啦5', `updated_at` = '2020-12-23 16:17:21'  WHERE `gocrud_todo`.`deleted_at` IS NULL AND ((id = 3))
				todo.ID = idUint
				c.JSON(http.StatusOK, gin.H{
					"msg": "success",
					"code": 200,
					"data": todo,
				})
			}
		}
		
	}

}

func deleteTodoHandler(c *gin.Context) {
	id := c.Param("id")
	idInt, _ := strconv.Atoi(id)
	// gorm 默认的删除方式不是真正的删除，是 deleted_at 为空的时候更新 deleted_at 字段，所以更新是”成功“ 的
	// 所以需要提前判断 待删除的记录是否存在，不存在则直接返回报错
	_, err = GetTodoService(idInt)
	// 更新的如果不存在
	if err != nil {
		c.JSON(http.StatusOK, gin.H{
			"msg": "待删除记录不存在",
			"code": 5007,
			"data": "",
		})
	} else {
		if err := DeleteTodoService(idInt); err != nil {
			c.JSON(http.StatusOK, gin.H{
				"msg": "删除失败",
				"code": 5006,
				"data": "",
			})
		} else {
			c.JSON(http.StatusOK, gin.H{
				"msg": "删除成功",
				"code": 200,
				"data": "",
			})
		}
	}
	
}
func listTodoHandler(c *gin.Context) {
	var (
		todos []*Todo
		err error
	)

	todos, err = ListTodoService()
	if err != nil {
		fmt.Println("接口获取列表失败 ", err)
		c.JSON(http.StatusOK, gin.H{
			"msg": "获取列表失败",
			"code": 5002,
			"data": "",
		})
	} else {
		c.JSON(http.StatusOK, gin.H{
			"msg": "success",
			"code": 200,
			"data": &todos,
		})
	}
}

// router 
func MapRouter() *gin.Engine {
	router := gin.Default()
	
	router.GET("/", indexHandler)
	router.GET("/todo/", listTodoHandler)
	router.POST("/todo/", addTodoHandler)
	router.PUT("/todo/:id", updateTodoHandler)
	// router.PATCH("/todo/:id", updateTodoHandler)
	router.DELETE("/todo/:id", deleteTodoHandler)
	router.GET("/todo/:id", getTodoHandler)
	return router
}


// main enterance
func main() {
	ConnectDB()
	defer DisconnectDB()

	db.AutoMigrate(&Todo{})
	
	router := MapRouter()
	router.Run()
}
